#ifdef __APPLE__
#include <OpenGL/OpenGL.h>
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif

#include <iostream>
#include <stdlib.h>
#include <cstdlib>
#include <math.h>
#include <vector>

#include "camera.h"
#include "color.h"

using namespace std;


#define PI 3.1415926535897932384626433832795028841971

//Global Variables
int angle = 1;           	//The rotation of the shape around the axis (Do not change)
#define max_angle 100		//number of iterations around (changable)
int max_rotation = 2 * max_angle; //because the camera is going to move for 2 iters (Do not change)
#define num_intervals 100		//number of iterations of depth (changable)
int rotation = 1;			 //camera rotation (Do not change)
float start = 2;			//where depth starts (changable)
float end = 10;			//where depth ends (changable)
bool axes = false;			//print axes? (toggle with keypress)
bool auto_rotate = true;		//auto-rotate the camera? (toggle with keypress)
bool l = false;			//draw grid? (toggle with keypress)
Color color;				//keeps track of color
int c = 1;				//keeps track of which color scheme is used
int f = 1	;				//keeps track of which function set is being used
int fprev = 0;				//keeps track of when to print new function values

PerspectiveCamera* camera;

//interactive mouse controls
int mouseButton;
int mouseX = 0;
int mouseY = 0;
bool controlPressed;

void mouse(int button, int state, int x, int y) {
  // Save the current state of the mouse.  This will be
  // used by the 'motion' function
  mouseButton = button;
  mouseX = x;
  mouseY = y;
  controlPressed = glutGetModifiers() & GLUT_ACTIVE_CTRL;
}

void motion(int x, int y) {

	if(!auto_rotate){
	  // Left button = rotation
	  // (rotate camera around the up and horizontal vectors)
	   if (mouseButton == GLUT_LEFT_BUTTON) {
		camera->rotateCamera(0.005*(mouseX-x), 0.005*(mouseY-y));
	   }
	   // Middle button = translation
	   // (move camera perpendicular to the direction vector)
	   else if (mouseButton == GLUT_MIDDLE_BUTTON) {
		camera->truckCamera((mouseX-x)*0.5, (y-mouseY)*0.5);
	   }
	   // Right button = dolly or zoom
	   // (move camera along the direction vector)
	   else if (mouseButton == GLUT_RIGHT_BUTTON) {
		if (controlPressed) {
		  camera->zoomCamera(mouseY-y);
		} else {
		  camera->dollyCamera(mouseY-y);
		}
	   }

	   mouseX = x;
	   mouseY = y;

	   // Redraw the scene with the new camera parameters
	   glutPostRedisplay();
	}

}

int HandleGLError() {
  	GLenum error;
  	int i = 0;
  	while ((error = glGetError()) != GL_NO_ERROR) {
  		printf ("GL ERROR(%d):  %s\n", i, gluErrorString(error));
  		i++;
  	}
  	if (i == 0) return 1;
  	return 0;
}


inline Vec3f ComputeNormal(const Vec3f &p1, const Vec3f &p2, const Vec3f &p3) {
	//Computes a normal vector from 3 points in a plane
	Vec3f v12 = p2;
	v12 -= p1;
  	Vec3f v23 = p3;
  	v23 -= p2;
  	Vec3f normal;
  	Vec3f::Cross3(normal,v12,v23);
  	normal.Normalize();
  	return normal;
}

void InsertNormal(const Vec3f &p1, const Vec3f &p2, const Vec3f &p3) {
	Vec3f normal = ComputeNormal(p1,p2,p3);
	glNormal3f(normal.x(),normal.y(),normal.z());
}

void changecolor(){
	c += 1;
	if(c > 4) c = c - 4;
	switch (c) {
		case 1: //Default
			color.setendcolor(1.0f, 0.6f, 0.8f);
			color.setoutsidecolor(0.67f, 0.0f, 1.0f);
			color.setinsidecolor(1.0f, 0.01f, 0.05f);
			color.setsidecolor(0.6f, 0.8f, 1.0f);
			break;
		case 2: //scheme 2
			color.setendcolor(0.851f, 0.367f, 0.263f);
			color.setoutsidecolor(0.325f, 0.467f, 0.478f);
			color.setinsidecolor(0.753f, 0.161f, 0.259f);
			color.setsidecolor(0.329f, 0.141f, 0.216f);
			break;
		case 3: //scheme 3
			color.setendcolor(0.780f, 0.957f, 0.392f);
			color.setoutsidecolor(0.333f, 0.384f, 0.439f);
			color.setinsidecolor(0.306f, 0.804f, 0.769f);
			color.setsidecolor(0.769f, 0.302f, 0.345f);
			break;
		case 4: //scheme 4
			color.setendcolor(0.929f, 0.725f, 0.180f);
			color.setoutsidecolor(0.973f, 0.349f, 0.192f);
			color.setinsidecolor(0.808f, 0.094f, 0.212f);
			color.setsidecolor(0.0f, 0.6f, 0.537f);
			break;
	}
	
}
void calcCrossSection(bool function, float depth, vector<Vec3f>& points, int f){
	/* This function calculates the value of either the inner or outer function
		at a particular point.  A 'function' value of 1 cues this to use the outer
		funcion, anything else means the inner funtion.  Becuase of the properties
		of a rotated function, the distance from the axis is constant of the same
		x value, so I use a unit circle multiplied by the function value. 
	*/
	float r = 0; //radius at this point
	//FUNCTION DEFINITIONS HERE: (make sure outer function is consistantly larger
	//than smaller function on chosen interval.
	switch (f) { //picks which funtion to draw
		case 1: //Default	
			if(function == true) r = 3*sqrt(abs(depth)); //outer function (changable)
			else r= abs(depth) *.40;				//inner function (changable)
			if(fprev != f){
				cout << "Outer function: y = 3 * x^(1/2)" << endl
					<< "Inner function: y = .4 * x" << endl;
			}
			fprev = f;
			break;
		case 2:
			if(function == true) r = sin(depth) + sqrt(abs(depth)) +2 ;
			else r = sqrt(abs(depth));
			if(fprev != f){
				cout << "Outer function: y = sin(x) + x^(1/2) +2 " << endl
					<< "Inner function: y = x^(1/2)" << endl;
			}
			fprev = f;
			break;
		case 3:
			if(function == true) r = 3* (abs(depth)/2 - floor(abs(depth)/2) +.5) + 1;
			else r = .5;
			if(fprev != f){
				cout << "Outer function: y = 3 | x/2 + floor(x/2) + 1/2| +1" << endl
					<< "Inner function: y = 1/2" << endl;
			}
			fprev = f;
			break;
		case 4:
			if(function == true) r = .1 * pow(abs(depth),2);
			else r = 0;
			if(fprev != f){
				cout << "Outer function: y = .1 * x^2" << endl
					<< "Inner function: y = 0" << endl;
			}
			fprev = f;
			break;
		case 5:
			if(depth < .01) depth = .001;
			if(function == true) r = 10* sin(abs(depth))/abs(depth);
			else r = 0;
			if(fprev != f){
				cout << "Outer function: y = sin(x)/x" << endl
					<< "Inner function: y = 0" << endl;
			}
			fprev = f;
			break;
	}
	float max = float(max_angle);
     for(int i=0;i<max_angle;i++){
		float y, z;
     	z = r*cos(i*2*PI/max); //z axis
     	y = r*sin(i*2*PI/max); //y axis (depth is x axis)
		Vec3f temp(depth, y, z);
		points.push_back(temp);
      }
}

void drawLines(Vec3f a, Vec3f b, Vec3f c, Vec3f d, Vec3f e, Vec3f f, Vec3f g, Vec3f h,
	      bool draw_top, bool draw_bottom, bool draw_left, bool draw_right){
	/*This function draws the wire frame on the function's surface
	*/
	/* This function draws a single cube with various color sides based on 8 
		input corners.
	*/

	glBegin(GL_LINES);
	glColor3f(0.0f, 0.0f, 0.0f);
	//I don't think I want this.

	glEnd();

}

void drawCube(Vec3f a, Vec3f b, Vec3f c, Vec3f d, Vec3f e, Vec3f f, Vec3f g, Vec3f h,
	      bool draw_top, bool draw_bottom, bool draw_left, bool draw_right){
	/* This function draws a single cube with various color sides based on 8 
		input corners.
	*/

	glBegin(GL_QUADS);
	glColor3f(color.getend().r(), color.getend().g(), color.getend().b());
	if (draw_bottom) {
	  //draw the bottom
	  InsertNormal(d,c,b);
	  glVertex3f(d.x(), d.y(), d.z());
	  glVertex3f(c.x(), c.y(), c.z());
	  glVertex3f(b.x(), b.y(), b.z());
	  glVertex3f(a.x(), a.y(), a.z());
	}

	if (draw_top) {
	  //draw the top
	  InsertNormal(e,f,g);
	  glVertex3f(e.x(), e.y(), e.z());
	  glVertex3f(f.x(), f.y(), f.z());
	  glVertex3f(g.x(), g.y(), g.z());
	  glVertex3f(h.x(), h.y(), h.z());
	}

	glColor3f(color.getside().r(), color.getside().g(), color.getside().b());
	if (draw_left) {
	  //draw the left
	  InsertNormal(e,h,d);
	  glVertex3f(e.x(), e.y(), e.z());
	  glVertex3f(h.x(), h.y(), h.z());
	  glVertex3f(d.x(), d.y(), d.z());
	  glVertex3f(a.x(), a.y(), a.z());
	}
	if (draw_right) {
	  //draw the right
	  InsertNormal(b,c,g);
	  glVertex3f(b.x(), b.y(), b.z());
	  glVertex3f(c.x(), c.y(), c.z());
	  glVertex3f(g.x(), g.y(), g.z());
	  glVertex3f(f.x(), f.y(), f.z());
	}

	glColor3f(color.getoutside().r(), color.getoutside().g(), color.getoutside().b());
	//draw the front
	InsertNormal(a,b,f);
	glVertex3f(a.x(), a.y(), a.z());
	glVertex3f(b.x(), b.y(), b.z());
	glVertex3f(f.x(), f.y(), f.z());
	glVertex3f(e.x(), e.y(), e.z());

	glColor3f(color.getinside().r(), color.getinside().g(), color.getinside().b());
	//draw the back
	InsertNormal(h,g,c);
	glVertex3f(h.x(), h.y(), h.z());
	glVertex3f(g.x(), g.y(), g.z());
	glVertex3f(c.x(), c.y(), c.z());
	glVertex3f(d.x(), d.y(), d.z());
	glEnd();

}

void drawCubes(vector<Vec3f> sxouter, vector<Vec3f> sxinner, vector<Vec3f> lxouter, vector<Vec3f> lxinner,
	       bool draw_top, bool draw_bottom, bool lines){
	/*This function draws one layer of cubes (from two adjascent x values).  For each
		individual cube it calls the draw cube function.
		lx = larger x
		sx = smaller x
		outer = outer function
		inner = inner function
		Each Vec3f stores a 3D coordinate for a point.
	*/
	Vec3f a, b, c, d, e, f, g, h;
	for(int i = 0; i < angle; i++){ 
		a = lxouter[i];			//i is how far around the circle we are
		b = lxouter[(i+1)%max_angle];	//i+1 is the next row of the circle (mod circle size to 
								//wrap around
		c = lxinner[(i+1)%max_angle];
		d = lxinner[i];
		e = sxouter[i];
		f = sxouter[(i+1)%max_angle];
		g = sxinner[(i+1)%max_angle];
		h = sxinner[i];

		bool draw_left = (i==0);
		bool draw_right = (i==angle-1);
		if(lines == true){
			drawLines(a, b, c, d, e, f, g, h, draw_top,draw_bottom,draw_left,draw_right);
		} 
		else{
			drawCube(a, b, c, d, e, f, g, h, draw_top,draw_bottom,draw_left,draw_right);
		}
	}


}

void drawAllCubes(vector<vector<Vec3f> > innerpoints, vector<vector<Vec3f> > outerpoints, bool lines){
	/*This calls the drawCubes function for each layer that needs to be drawn.
		The num_intervals variable is how many layers the user would like the 
		function to be divided into in the x direction.
	*/
	for(int i= 0; i < num_intervals-1; i++){
	  bool draw_top = (i == 0);
	  bool draw_bottom = (i == (num_intervals-2));
	  drawCubes(outerpoints[i], innerpoints[i], outerpoints[i+1], innerpoints[i+1],
		    draw_top,draw_bottom, lines);
	}
}

void handleKeypress(unsigned char key, int x, int y) {
	/*Takes key input and acts accordingly
	*/
	switch (key) {
		case 27: //Escape key
			exit(0);
			break;
		case 'q':
			exit(0);
			break;		
		case 'a': 
			axes = !axes;
			break;		
	     case 'r':
		     auto_rotate = !auto_rotate;
		     break;
		case 'c': 
			changecolor();
			break;
		case 'f':
			f += 1;
			if(f > 5) f -=5;
			break;
	}
}

void initRendering() {
	/*This function handles initializations and places the camera to start facing
		the original plane drawn
	*/
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);
	glEnable(GL_NORMALIZE);
	glEnable(GL_COLOR_MATERIAL);

	//beginning colors
	color.setendcolor(1.0f, 0.6f, 0.8f);
	color.setoutsidecolor(0.67f, 0.0f, 1.0f);
	color.setinsidecolor(1.0f, 0.01f, 0.05f);
	color.setsidecolor(0.6f, 0.8f, 1.0f);
	
	//beginning camera position
	const Vec3f camera_position = Vec3f(float((end-start)/2),-10,0);
	const Vec3f direction = Vec3f(float((end-start)/2),0,0);
	const Vec3f up = Vec3f(0,0,1);
	float scale = 3;
	camera = new PerspectiveCamera(camera_position, direction, up, scale);

}

void handleResize(int w, int h) {
	/*This handles when the window is resized.
	*/
	glViewport(0, 0, w, h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(45.0, (float)w / (float)h, 10.0, 100.0);
	glTranslatef(0.0f, 0.0f, -20.0f);
}

void drawScene() {

	HandleGLError(); 

	//background color
	glClearColor(1.0,1.0,1.0,0);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	

	// Set the camera parameters
  	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	camera->glPlaceCamera();

	if(auto_rotate){
		//camera turning code
		float x, y;
		//turns the camera in the first direction on the first revolution
		if(rotation < max_rotation/2){	
			float current_rotation = (rotation * .005) +90; //.005 is distance traveled in one 
												   //iteration
			x = float((end-start)/2) + (10 * cos (-current_rotation));  //10 is distance away
			y = 10 * sin(-current_rotation);
		}
		//turns the camera in the second direction for the second revolution
		else {
			float current_rotation = (((rotation-max_rotation/2) * .005) - (max_rotation/2 * .005) -90);
			x = float((end-start)/2) + (10 * cos (current_rotation));
			y = 10 * sin(current_rotation);
		}
		//moves the camera to the coordinates calculated above

		const Vec3f camera_position = Vec3f(x,y,0);
		const Vec3f direction = Vec3f(float((end-start)/2),0,0);
		const Vec3f up = Vec3f(0,0,1);
		float scale = 3;
		delete camera;		
		camera = new PerspectiveCamera(camera_position, direction, up, scale);
		//end camera turing code
	}
	
	
	//creates light

	glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);

	GLfloat position[4] = { -10,-30,30, 1};
	GLfloat diffuse[4] = { 0.7,0.7,0.7,1};
	GLfloat specular[4] = { 0.2,0.2,0.2,1};
	GLfloat ambient[4] = { 0.3, 0.3, 0.3, 1.0 };

	glLightfv(GL_LIGHT0, GL_POSITION, position);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
	glLightfv(GL_LIGHT0, GL_SPECULAR, specular);
	glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);

	glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);

	GLfloat spec_mat[4] = {1,1,1,1};
	float glexponent = 10;
	glMaterialfv(GL_FRONT, GL_SHININESS, &glexponent);
	glMaterialfv(GL_FRONT, GL_SPECULAR, spec_mat);

	glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
	float back_color[] = { 0.2,0.8,0.8,1};
	glMaterialfv(GL_BACK, GL_AMBIENT_AND_DIFFUSE, back_color);
	glEnable(GL_LIGHT0);


	/*The following block commented code draws in coordinate lines that
		are 40 units long.
	*/

	
	//start lines code
	if(axes == true){
		float eps = .03;
		float length = 2* abs(max(end, start));
		glBegin(GL_LINES);
		//x line is blue (dark is positive)	
		glColor3f(0.6f, 0.8f, 1.0f);
		glVertex3f(-length, -eps, 0.0f);
		glColor3f(0.0f, 0.2f, 0.4f);
		glVertex3f(length, -eps, 0.0f);	
/*		
		//Goes straight out of the screen at cross section, doesn't add anything
		//y line is red (dark is positive)
		glColor3f(1.0f, 0.4f, 0.8f);
		glVertex3f(0.0f, -length, 0.0f);
		glColor3f(0.4f, 0.0f, 0.0f);
		glVertex3f(0.0f, length, 0.0f);
*/	
		//z line is green (dark is positive)	
		glColor3f(0.0f, 0.4f, 0.2f);
		glVertex3f(-eps, 0.0f, length);
		glColor3f(0.0f, 1.0f, 0.4f);
		glVertex3f(-eps, 0.0f, -length);
		glEnd();
	}
	//end lines code	
	

	//calculate cross sections 
	vector<vector<Vec3f> > innerpoints;
	vector<vector<Vec3f> > outerpoints;
	float depth = start;
	for(int i = 0; i < num_intervals; i++, depth += (end - start)/num_intervals){
		vector<Vec3f> temp;
		calcCrossSection(0, depth, temp, f); //0 for inner function
		innerpoints.push_back(temp);
		temp.clear();
		calcCrossSection(1, depth, temp, f); //1 for outer function
		outerpoints.push_back(temp);
	}
		
	//draw stuff
	bool lines = false; //draw actual shape or wire frame? (frame is true)
	drawAllCubes(innerpoints, outerpoints, lines);
	if(l == true){
		lines = true;
		drawAllCubes(innerpoints, outerpoints, lines);
	}
	glutSwapBuffers();
}

//Called every 25 milliseconds
void update(int value) {
	/*This is the function that animates the program.  The angle variable changes
		how much of the figure should be drawn and the rotation variable is
		the camera's position.
	*/

        angle += 1;
	if (angle > max_angle) {
		angle -= max_angle; //to keep in the range of 0 to max angle
	}
	if (auto_rotate) {
	  rotation += 1;
	}
	if(rotation > max_rotation){
		rotation -= max_rotation; //to keep in the range of 0 to max rotation
	}
	glutPostRedisplay();
	glutTimerFunc(25, update, 0);
}

int main(int argc, char** argv) {
	
	//how to use
	cout << "Toggle keys: " << endl 
	<< "c changes color scheme" << endl
	<< "f changes functions used" << endl
	<< "a turn drawn axes on and off" << endl
	<< "q and escape quit" << endl
	<< "r turns auto-rotate on and off" << endl
	<< "when auto-rotate is off mouse buttons move camera" << endl;
	
	//arg checks
	if(argc != 3){
		cout << "Command line arguments are: start, end." << endl;
		if(argc == 1){
			start = 2;
			end = 10;
		}
		else exit(0);
	}
	else{
		start = atoi(argv[1]);
		end = atoi(argv[2]);
	}
	if(start > end){
		cout << "Start should come before end. " << endl;
		int temp = start;
		start = end;
		end = temp;
	}


	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
	glutInitWindowSize(600, 400);
	
	glutCreateWindow("Rotation");
	initRendering();
	
	glutDisplayFunc(drawScene);
	glutKeyboardFunc(handleKeypress);
	glutReshapeFunc(handleResize);
	glutMouseFunc(mouse);
	glutMotionFunc(motion);
	glutTimerFunc(25, update, 0);

	glutMainLoop();
	return 0;
}

